package crmdna.registration;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import com.googlecode.objectify.VoidWork;
import com.googlecode.objectify.cmd.Query;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.common.Utils.PaypalErrorType;
import crmdna.contactdetail.ContactDetail;
import crmdna.contactdetail.ContactDetailProp;
import crmdna.group.Group;
import crmdna.group.PaypalApiCredentialsProp;
import crmdna.member.Member;
import crmdna.member.MemberCore.MemberProp;
import crmdna.payment.Payment;
import crmdna.program.Program;
import crmdna.program.ProgramProp;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;
import crmdna.user.User;

public class Registration {

	public enum RegistrationStatus {
		REQUEST_RECEIVED, PAYMENT_AUTHORIZATION_PENDING, ERROR_CREATING_PAYMENT_URL, PAYMENT_NOT_AUTHORIZED, PAYMENT_AUTHORIZED, PAYMENT_PENDING, REGISTRATION_COMPLETE, PAYPAL_ERROR
	}

	public static RegistrationProp register(String client, String firstName,
			String lastName, String nickName, String gender, String email,
			String mobilePhone, String homePhone, String officePhone,
			String country, String postalCode, String programIdStr,
			String feeStr, String batchNoStr, String marketingChannel,
			String successCallbackUrl, String errorCallbackUrl, String rootUrl,
			boolean sandbox) {

		ContactDetailProp contact = new ContactDetailProp();
		contact.firstName = firstName;
		contact.lastName = lastName;
		contact.nickName = nickName;
		contact.email = email;
		contact.mobilePhone = mobilePhone;
		contact.homePhone = homePhone;
		contact.officePhone = officePhone;
		contact.gender = ContactDetail.getGender(gender);

		StringBuilder homeAddress = new StringBuilder();
		if (null != country)
			homeAddress.append(country + " ");
		if (null != postalCode)
			homeAddress.append("Postal code: " + postalCode);
		contact.homeAddress = homeAddress.toString();

		if (null == programIdStr)
			Utils.throwIncorrectSpecException("programId should be specified");
		long programId = Utils.safeParseAsLong(programIdStr);

		Double fee = null;
		if (null != feeStr)
			fee = Utils.safeParseAsDouble(feeStr);

		int batchNo = 1;
		if (batchNoStr != null)
			batchNo = Utils.safeParseAsInt(batchNoStr);

		if ((null == successCallbackUrl) || successCallbackUrl.equals(""))
			Utils.throwIncorrectSpecException("successCallbackUrl is not specified");
		Utils.ensureValidUrl(successCallbackUrl);

		if (null == errorCallbackUrl)
			errorCallbackUrl = successCallbackUrl;
		Utils.ensureValidUrl(errorCallbackUrl);

		RegistrationProp registrationProp = register(client, contact,
				programId, batchNo, fee, marketingChannel, successCallbackUrl,
				errorCallbackUrl, rootUrl, sandbox);

		return registrationProp;
	}

	public static RegistrationEntity safeGet(String client, long registrationId) {
		Client.ensureValidClient(client);

		RegistrationEntity entity = ofy(client).load()
				.type(RegistrationEntity.class).id(registrationId).get();

		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST,
					"Cannot find Registration details for registration id ["
							+ registrationId + "]");

		return entity;
	}

	public static RegistrationProp register(String client,
			ContactDetailProp contact, long programId, int batchNo, Double fee,
			String marketingChannel, String successCallbackUrl,
			String errorCallbackUrl, String rootUrl, boolean sandbox) {

		Client.ensureValidClient(client);

		ProgramProp programProp = Program.safeGet(client, programId).toProp();
		if ((batchNo < 1) || (batchNo > programProp.numBatches))
			Utils.throwIncorrectSpecException("Batch number is invalid");

		// cannot register 30 days after the end of program
		// 30 days is just so that there is enough buffer
		int currentYYYYMMDD = Utils.toYYYYMMDD(new Date()); // current date
		Date current = new Date();
		Date end = Utils.toDate(programProp.endYYYYMMDD);

		if (Utils.getNumDays(end, current) > 30)
			throw new APIException(
					Status.ERROR_OPERATION_NOT_ALLOWED,
					"Registration not allowed as current date ["
							+ currentYYYYMMDD
							+ "] is more than 30 days after the program end date ["
							+ programProp.endYYYYMMDD + "]");

		ContactDetail.ensureFirstNameAndValidEmailSpecified(contact);
		contact.email = contact.email.toLowerCase();

		Long registrationId = getMatchingRegistrationId(client, programId,
				contact);

		RegistrationEntity registrationEntity;
		if (null == registrationId) {
			registrationEntity = new RegistrationEntity();
			registrationEntity.registrationId = Sequence.getNext(client,
					SequenceType.REGISTRANT);
		} else {
			registrationEntity = safeGet(client, registrationId);
			RegistrationProp registrationProp = registrationEntity.toProp();

			if (registrationProp.getStatus() == RegistrationStatus.REGISTRATION_COMPLETE) {
				registrationProp.alreadyRegistered = true;
				return registrationProp;
			}

			if (registrationProp.getStatus() == RegistrationStatus.PAYMENT_PENDING) {
				Logger logger = Logger.getLogger(Registration.class.getName());
				String errMessage = "Registration for email ["
						+ contact.email
						+ "] and Name ["
						+ contact.getName()
						+ "] is already with status [PAYMENT_PENDING] and registration Id ["
						+ registrationProp.registrationId
						+ "]. Now a new registration for the same name and "
						+ "email is being attempted. This could lead to duplicate registration and "
						+ "duplicate payments.";
				logger.warning(errMessage);
			}

			// clear off the paypal error/payment fields
			registrationEntity.amount = null;
			registrationEntity.ccy = null;
			registrationEntity.isPaymentPending = false;
			registrationEntity.L_ERRORCODE0 = null;
			registrationEntity.L_LONGMESSAGE0 = null;
			registrationEntity.L_SEVERITYCODE0 = null;
			registrationEntity.L_SHORTMESSAGE0 = null;
			registrationEntity.redirectUrl = null;
		}

		Long memberId = Member.getMatchingMemberId(client, contact.email,
				contact.firstName, contact.lastName);
		if (memberId == null) {
			MemberProp memberProp = Member.createAllowDuplicateEmail(client,
					programProp.groupProp.groupId, contact, User.SUPER_USER);
			memberId = memberProp.memberId;
		}

		populateContactDetailsAndQSTags(registrationEntity, contact);
		registrationEntity.memberId = memberId;
		registrationEntity.programId = programId;
		registrationEntity.marketingChannel = marketingChannel;
		registrationEntity.successCallbackUrl = successCallbackUrl;
		registrationEntity.errorCallbackUrl = errorCallbackUrl;

		Logger logger = Logger.getLogger(Registration.class.getName());
		logger.info("registrationId = [" + registrationEntity.registrationId
				+ "], memberId = [" + registrationEntity.memberId
				+ "], programId = [" + registrationEntity.programId + "]");

		registrationEntity
				.recordStateChange(RegistrationStatus.REQUEST_RECEIVED);
		ofy(client).save().entity(registrationEntity).now();

		fee = getFee(fee, programProp.fee);

		if (programProp.fee == 0) {
			registrationEntity
					.recordStateChange(RegistrationStatus.REGISTRATION_COMPLETE);
			ofy(client).save().entity(registrationEntity).now();
			return registrationEntity.toProp();
		}

		// payment is involved - ensure group is set to receive payment
		PaypalApiCredentialsProp prop = Group.getPaypalApiCredentials(client,
				programProp.groupProp.groupId, User.SUPER_USER);

		if ((null == prop) || prop.disable)
			throw new APIException(Status.ERROR_OPERATION_NOT_ALLOWED,
					"Paypal info is either not available or payments are disabled for group ["
							+ programProp.groupProp.displayName + "]");

		String paymentName = programProp.programTypeProp.displayName
				+ Utils.getDurationAsString(programProp.startYYYYMMDD,
						programProp.endYYYYMMDD);
		String invoiceNo = Invoice.getInvoiceNo(
				programProp.programTypeProp.displayName,
				programProp.groupProp.displayName,
				registrationEntity.registrationId);
		// TODO: do not allow _ in group and program prop or use : as delimiter

		// do a paypal set express checkout and get the payment url
		try {
			String paymentUrl = Payment.setExpressCheckoutAndGetPaymentUrl(
					client, contact.email, paymentName, invoiceNo, prop.login,
					prop.pwd, prop.secret, fee, programProp.ccy.toString(),
					rootUrl, sandbox);

			registrationEntity.paymentUrl = paymentUrl;
			registrationEntity
					.recordStateChange(RegistrationStatus.PAYMENT_AUTHORIZATION_PENDING);
			ofy(client).save().entity(registrationEntity).now();
			return registrationEntity.toProp();

		} catch (APIException ex) {
			registrationEntity
					.recordStateChange(RegistrationStatus.ERROR_CREATING_PAYMENT_URL);
			ofy(client).save().entity(registrationEntity).now();
			throw ex;
		}
	}

	static double getFee(Double override, double programFee) {
		double fee = programFee;

		if (override != null)
			fee = override;

		return fee;
	}

	static void populateContactDetailsAndQSTags(
			RegistrationEntity registrationEntity, ContactDetailProp contact) {
		if (contact.email != null) {
			Utils.ensureValidEmail(contact.email);
			registrationEntity.email = contact.email.toLowerCase();
		}

		if (contact.firstName != null)
			registrationEntity.firstName = contact.firstName;

		if (contact.lastName != null)
			registrationEntity.lastName = contact.lastName;

		if (contact.nickName != null)
			registrationEntity.nickName = contact.nickName;

		if (contact.homeAddress != null)
			registrationEntity.homeAddress = contact.homeAddress;

		if (contact.officeAddress != null)
			registrationEntity.officeAddress = contact.officeAddress;

		if (contact.homePhone != null) {
			Utils.ensureValidPhoneNumber(contact.homePhone);
			registrationEntity.homePhone = contact.homePhone;
		}

		if (contact.officePhone != null) {
			Utils.ensureValidPhoneNumber(contact.officePhone);
			registrationEntity.officePhone = contact.officePhone;
		}

		if (contact.mobilePhone != null) {
			Utils.ensureValidPhoneNumber(contact.mobilePhone);
			registrationEntity.mobilePhone = contact.mobilePhone;
		}

		registrationEntity.qsTags = Utils.getQSTags(contact.firstName,
				contact.lastName, contact.nickName, contact.email,
				contact.mobilePhone, contact.homePhone, contact.officePhone);
	}

	static void ensureNotDuplicate_notused(String client, long programId,
			ContactDetailProp contact) {

		ContactDetail.ensureFirstNameAndValidEmailSpecified(contact);
		contact.email = contact.email.toLowerCase();

		RegistrationQueryCondition queryCondition = new RegistrationQueryCondition();
		queryCondition.programId = programId;
		queryCondition.searchStr = contact.email;
		queryCondition.status = RegistrationStatus.REGISTRATION_COMPLETE;

		List<RegistrationProp> props = query(client, queryCondition,
				User.SUPER_USER);

		for (RegistrationProp prop : props) {
			if (prop.getName().toLowerCase()
					.equals(contact.getName().toLowerCase())) {
				throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
						contact.getName() + " with email [" + contact.email
								+ " ] is already registered for this program");
			}
		}
	}

	static Long getMatchingRegistrationIdEnsuringNotDuplicate_notused(
			String client, long programId, ContactDetailProp contact) {

		ContactDetail.ensureFirstNameAndValidEmailSpecified(contact);
		contact.email = contact.email.toLowerCase();

		RegistrationQueryCondition queryCondition = new RegistrationQueryCondition();
		queryCondition.programId = programId;
		queryCondition.searchStr = contact.email;
		// queryCondition.status = RegistrationStatus.REGISTRATION_COMPLETE;

		List<RegistrationProp> props = query(client, queryCondition,
				User.SUPER_USER);

		for (RegistrationProp prop : props) {
			if (Utils.closeEnough(prop.getName().toLowerCase(), contact
					.getName().toLowerCase())) {

				if (prop.getStatus() == RegistrationStatus.REGISTRATION_COMPLETE)
					throw new APIException(
							Status.ERROR_RESOURCE_ALREADY_EXISTS,
							contact.getName()
									+ " with email ["
									+ contact.email
									+ "] is already registered for this program");

				// if existing entity status is PAYMENT_PENDING, we could
				// potentially get payment (through ipn) in the future.
				// For such cases do not return the matching registration ID;
				// allow the system to create a new registration ID
				// TODO: verify this with Thulasi

				if (prop.getStatus() == RegistrationStatus.PAYMENT_PENDING) {
					Logger logger = Logger.getLogger(Registration.class
							.getName());
					String errMessage = "Registration for email ["
							+ contact.email
							+ "] and Name ["
							+ contact.getName()
							+ "] is already with status [PAYMENT_PENDING] and registration Id ["
							+ prop.registrationId
							+ "]. Now a new registration for the same name and "
							+ ""
							+ "email is being attempted. This could lead to duplicate registration and "
							+ "duplicate payments.";
					logger.warning(errMessage);
				} else {
					return prop.registrationId;
				}
			}
		}

		// no matching registration id, doing it the first time
		return null;
	}

	static Long getMatchingRegistrationId(String client, long programId,
			ContactDetailProp contact) {

		ContactDetail.ensureFirstNameAndValidEmailSpecified(contact);
		contact.email = contact.email.toLowerCase();

		RegistrationQueryCondition queryCondition = new RegistrationQueryCondition();
		queryCondition.programId = programId;
		queryCondition.searchStr = contact.email;

		List<RegistrationProp> props = query(client, queryCondition,
				User.SUPER_USER);

		for (RegistrationProp prop : props) {
			if (Utils.closeEnough(prop.getName().toLowerCase(), contact
					.getName().toLowerCase())) {
				return prop.registrationId;
			}
		}

		// no matching registration id. return null
		return null;
	}

	static List<RegistrationProp> query(String client,
			RegistrationQueryCondition qc, String login) {

		Client.ensureValidClient(client);
		User.ensureValidUser(client, login);

		Query<RegistrationEntity> q = ofy(client).load().type(
				RegistrationEntity.class);

		if (null != qc.programId)
			q = q.filter("programId", qc.programId);

		if (null != qc.status) {
			q = q.filter("status", qc.status);
		}

		if (null != qc.searchStr) {
			if (qc.searchStr.length() < 3)
				Utils.throwIncorrectSpecException("Search string when specified should be atleast 3 char");
			qc.searchStr = qc.searchStr.toLowerCase();
			Set<String> searchTags = Utils.getQSTags(qc.searchStr);
			for (String tag : searchTags) {
				q = q.filter("qsTags", tag);
			}
		}

		List<RegistrationEntity> entities = q.list();

		List<RegistrationProp> props = new ArrayList<>();
		for (RegistrationEntity entity : entities)
			props.add(entity.toProp());

		if (qc.sortByFirstName)
			sortByFirstName(props);

		return props;
	}

	static RegistrationProp queryByTransactionId(String client,
			String transactionId) {
		Client.ensureValidClient(client);

		Query<RegistrationEntity> q = ofy(client).load()
				.type(RegistrationEntity.class)
				.filter("transactionId", transactionId);

		List<RegistrationEntity> entities = q.list();

		if (entities.size() == 0)
			return null;

		if (entities.size() > 1) {
			// should never happen
			Logger logger = Logger.getLogger(Registration.class.getName());
			logger.severe("Found [" + entities.size()
					+ "] registration entities " + "with transaction id ["
					+ transactionId + "]");
		}

		return entities.get(0).toProp();
	}

	static RegistrationProp getByTransactionId(String client,
			String transactionId) {
		Client.ensureValidClient(client);

		TransactionEntity transactionEntity = ofy(client).load()
				.type(TransactionEntity.class).id(transactionId).get();

		if (transactionEntity == null)
			return null;

		long registrationId = transactionEntity.registrationId;

		RegistrationProp registrationProp = safeGet(client, registrationId)
				.toProp();

		return registrationProp;
	}

	static RegistrationStatusProp queryRegistrationStatus(String client,
			long programId, String email, String firstName, String lastName) {

		Client.ensureValidClient(client);

		ContactDetailProp contactDetailProp = new ContactDetailProp();
		contactDetailProp.firstName = firstName;
		contactDetailProp.lastName = lastName;
		contactDetailProp.email = email;

		ContactDetail.ensureFirstNameAndValidEmailSpecified(contactDetailProp);

		Long registrationId = getMatchingRegistrationId(client, programId,
				contactDetailProp);

		if (registrationId == null) {
			RegistrationStatusProp rsp = new RegistrationStatusProp();
			rsp.alreadyRegistered = false;
			rsp.hasAttemptedRegistrationBefore = false;
			return rsp;
		}

		RegistrationStatusProp rsp = new RegistrationStatusProp();
		rsp.hasAttemptedRegistrationBefore = true;

		RegistrationProp rp = Registration.safeGet(client, registrationId)
				.toProp();
		rsp.lastRegistrationStatus = rp.getStatus();
		rsp.registrationStatusTimestamp = rp.getStatusTimestamp();

		if (rsp.lastRegistrationStatus == RegistrationStatus.REGISTRATION_COMPLETE)
			rsp.alreadyRegistered = true;

		return rsp;

	}

	static RegistrationSummaryProp getSummary(String client, long programId,
			String login) {

		Client.ensureValidClient(client);
		User.ensureValidUser(client, login);
		Program.safeGet(client, programId);

		RegistrationSummaryProp rsp = new RegistrationSummaryProp();

		RegistrationQueryCondition qc = new RegistrationQueryCondition();
		qc.programId = programId;

		List<RegistrationProp> registrationProps = query(client, qc, login);

		for (RegistrationProp registrationProp : registrationProps) {
			RegistrationStatus status = registrationProp.getStatus();

			if (status == RegistrationStatus.REGISTRATION_COMPLETE) {
				rsp.numCompleted++;
			} else {
				rsp.numStartedButNotCompleted++;
			}

			if (!rsp.regStatusVsNum.containsKey(status))
				rsp.regStatusVsNum.put(status, 1);
			else {
				int current = rsp.regStatusVsNum.get(status);
				rsp.regStatusVsNum.put(status, current + 1);
			}

			int yyyymmdd = Utils.toYYYYMMDD(registrationProp
					.getStatusTimestamp());

			if (status == RegistrationStatus.REGISTRATION_COMPLETE) {

				if (!rsp.yyyymmddVsNumCompleted.containsKey(yyyymmdd))
					rsp.yyyymmddVsNumCompleted.put(yyyymmdd, 1);
				else {
					int current = rsp.yyyymmddVsNumCompleted.get(yyyymmdd);
					rsp.yyyymmddVsNumCompleted.put(yyyymmdd, current + 1);
				}

			} else {
				if (!rsp.yyyymmddVsNumStartedButNotCompleted
						.containsKey(yyyymmdd))
					rsp.yyyymmddVsNumStartedButNotCompleted.put(yyyymmdd, 1);
				else {
					int current = rsp.yyyymmddVsNumStartedButNotCompleted
							.get(yyyymmdd);
					rsp.yyyymmddVsNumStartedButNotCompleted.put(yyyymmdd,
							current + 1);
				}
			}
		}

		return rsp;
	}

	public static void handlePaymentAuthorizationFailure(String client,
			long registrationId, String redirectUrl) {
		Client.ensureValidClient(client);

		safeGet(client, registrationId);

		RegistrationEntity entity = safeGet(client, registrationId);
		entity.recordStateChange(RegistrationStatus.PAYMENT_NOT_AUTHORIZED);
		entity.redirectUrl = redirectUrl;
		ofy(client).save().entity(entity);
	}

	public static void handlePaypalError(String client, long registrationId,
			PaypalErrorType paypalErrorType, String errorCode0,
			String longMessage0, String severityCode0, String shortMessage0,
			String redirectUrl) {

		Client.ensureValidClient(client);
		RegistrationEntity entity = safeGet(client, registrationId);

		entity.paypalErrorType = paypalErrorType;
		entity.L_ERRORCODE0 = errorCode0;
		entity.L_LONGMESSAGE0 = longMessage0;
		entity.L_SEVERITYCODE0 = severityCode0;
		entity.L_SHORTMESSAGE0 = shortMessage0;
		entity.redirectUrl = redirectUrl;

		entity.recordStateChange(RegistrationStatus.PAYPAL_ERROR);

		ofy(client).save().entity(entity).now();
	}

	public static void handleDoExpressCheckoutResponse(final String client,
			final long registrationId, final String amount, final String ccy,
			final String transactionId, final boolean isPaymentPending,
			final String pendingReason, final String redirectUrl) {

		Client.ensureValidClient(client);

		final Logger logger = Logger.getLogger(Registration.class.getName());

		ofy(client).transact(new VoidWork() {

			@Override
			public void vrun() {
				RegistrationEntity registrationEntity = safeGet(client,
						registrationId);

				registrationEntity.amount = amount;
				registrationEntity.ccy = ccy;
				registrationEntity.pendingReason = pendingReason;
				registrationEntity.redirectUrl = redirectUrl;

				RegistrationStatus status;
				if (isPaymentPending) {
					status = RegistrationStatus.PAYMENT_PENDING;
				} else {
					status = RegistrationStatus.REGISTRATION_COMPLETE;
				}

				registrationEntity.recordStateChange(status);
				registrationEntity.transactionId = transactionId;

				// transaction entity
				TransactionEntity transactionEntity = new TransactionEntity();
				transactionEntity.transactionId = transactionId;
				transactionEntity.registrationId = registrationId;

				ofy(client).save()
						.entities(registrationEntity, transactionEntity).now();

				logger.info("Registration ID [" + registrationId + "] + "
						+ ", Status [" + status + "], transactionId ["
						+ transactionId + "], Redirect URL [" + redirectUrl
						+ "]");
			}
		});
	}

	public static void handlePaymentAuthorization(String client,
			long registrationId) {
		Client.ensureValidClient(client);

		safeGet(client, registrationId);

		RegistrationEntity entity = safeGet(client, registrationId);
		entity.recordStateChange(RegistrationStatus.PAYMENT_AUTHORIZED);
		ofy(client).save().entity(entity).now();
	}

	static void sortByFirstName(List<RegistrationProp> props) {
		Collections.sort(props, new Comparator<RegistrationProp>() {

			@Override
			public int compare(RegistrationProp o1, RegistrationProp o2) {
				if ((o1.firstName == null) || (o2.firstName == null))
					return 0;

				return o1.firstName.compareTo(o2.firstName);
			}
		});
	}
}
